iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
自我挑戰組

自學vue~點亮Roadmap過程系列 第 16

vue3鍊成術第十六天-響應式基礎補充

  • 分享至 

  • xImage
  •  

響應式基礎補充

討論:為何要使用ref()而非普通的變量?

為什麼我們需要使用帶有 .value 的 ref,而不是普通的變量?

當你在模板中使用了一個 ref,然後改變了這個 ref 的值時,Vue 會自動檢測到這個變化,並且相應地更新 DOM。這是通過一個基於依賴追蹤的響應式系統實現的。當一個組件首次渲染時,Vue 會追蹤在渲染過程中使用的每一個 ref。然後,當一個 ref 被修改時,它會觸發追蹤它的組件的一次重新渲染。

在標準的 JavaScript 中,檢測普通變量的訪問或修改是行不通的。然而,我們可以通過 getter 和 setter 方法來攔截對象屬性的 get 和 set 操作。

該 .value 屬性給予了 Vue 一個機會來檢測 ref 何時被訪問或修改。在其內部,Vue 在它的 getter 中執行追蹤,在它的 setter 中執行觸發。從概念上講,你可以將 ref 看作是一個像這樣的對象:

// 偽代碼,並非真正的實現
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

另一個 ref 的好處是,與普通變量不同,你可以將 ref 傳遞給函數,同時保留對最新值和響應式連接的訪問。當將複雜的邏輯重構為可重用的代碼時,這將非常有用。

深層響應性

Ref 可以持有任何類型的值,包括深層嵌套的對象、數組或者 JavaScript 內置的數據結構,例如 Map。
Ref 會使它的值具有深層響應性。這意味著即使改變嵌套對象或數組時,變化也會被檢測到:

import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都會按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

也可以通過 shallow ref 來放棄深層響應性。對於淺層 ref,只有 .value 的訪問會被追蹤。淺層 ref 可以用於避免對大型數據的響應性開銷來優化性能、或者有外部庫管理其內部狀態的情況。

DOM 更新時機

當你修改了響應式狀態時,DOM 會被自動更新。但是需要注意的是,DOM 更新不是同步的。Vue 會在“next tick”更新週期中緩衝所有狀態的修改,以確保不管你進行了多少次狀態修改,每個組件都只會被更新一次。

要等待 DOM 更新完成後再執行額外的代碼,可以使用 nextTick() 全局 API:

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 現在 DOM 已經更新了
}

reactive()

還有另一種聲明響應式狀態的方式,即使用 reactive() API。與將內部值包裝在特殊對象中的 ref 不同,reactive() 將使對象本身具有響應性。
響應式對象是 JavaScript 代理,其行為就和普通對象一樣。不同的是,Vue 能夠攔截對響應式對象所有屬性的訪問和修改,以便進行依賴追蹤和觸發更新。

reactive() 將深層地轉換對象:當訪問嵌套對象時,它們也會被 reactive() 包裝。當 ref 的值是一個對象時,ref() 也會在內部調用它。與淺層 ref 類似,這裡也有一個 shallowReactive() API 可以選擇退出深層響應性。

Reactive Proxy vs. Original
值得注意的是,reactive() 返回的是一個原始對象的 Proxy,它和原始對象是不相等的:

const raw = {}
const proxy = reactive(raw)

// 代理對象和原始對象不是全等的
console.log(proxy === raw) // false

只有代理對象是響應式的,更改原始對象不會觸發更新。因此,使用 Vue 的響應式系統的最佳實踐是僅使用你聲明對象的代理版本。

為保證訪問代理的一致性,對同一個原始對象調用 reactive() 會總是返回同樣的代理對象,而對一個已存在的代理對象調用 reactive() 會返回其本身:

// 在同一個對象上調用 reactive() 會返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一個代理上調用 reactive() 會返回它自己
console.log(reactive(proxy) === proxy) // true

這個規則對嵌套對象也適用。依靠深層響應性,響應式對象內的嵌套對象依然是代理:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

reactive() 的局限性

  1. **有限的值類型:**它只能用於對象類型 (對象、數組和如 Map、Set 這樣的集合類型)。它不能持有如 string、number 或 boolean 這樣的原始類型。
  2. **不能替換整個對象:**由於 Vue 的響應式跟蹤是通過屬性訪問實現的,因此我們必須始終保持對響應式對象的相同引用。這意味著我們不能輕易地“替換”響應式對象,因為這樣的話與第一個引用的響應性連接將丟失:
let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用將不再被追蹤
// (響應性連接已丟失!)
state = reactive({ count: 1 })
  1. **對解構操作不友好:**當我們將響應式對象的原始類型屬性解構為本地變量時,或者將該屬性傳遞給函數時,我們將丟失響應性連接:
const state = reactive({ count: 0 })

// 當解構時,count 已經與 state.count 斷開連接
let { count } = state
// 不會影響原始的 state
count++

// 該函數接收到的是一個普通的數字
// 並且無法追蹤 state.count 的變化
// 我們必須傳入整個對象以保持響應性
callSomeFunction(state.count)

額外的 ref 解包細節

作為 reactive 對象的屬性
一個 ref 會在作為響應式對象的屬性被訪問或修改時自動解包。換句話說,它的行為就像一個普通的屬性:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

如果將一個新的 ref 賦值給一個關聯了已有 ref 的屬性,那麼它會替換掉舊的 ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原始 ref 現在已經和 state.count 失去聯繫
console.log(count.value) // 1

只有當嵌套在一個深層響應式對象內時,才會發生 ref 解包。當其作為淺層響應式對象的屬性被訪問時不會解包。

數組和集合的注意事項

與 reactive 對象不同的是,當 ref 作為響應式數組或原生集合類型 (如 Map) 中的元素被訪問時,它不會被解包:

const books = reactive([ref('Vue 3 Guide')])
// 這裡需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 這裡需要 .value
console.log(map.get('count').value)

在Tamplate中解包的注意事項

在模板渲染上下文中,只有頂級的 ref 屬性才會被解包。

在下面的例子中,count 和 object 是頂級屬性,但 object.id 不是:

const count = ref(0)
const object = { id: ref(1) }

這可以成功解包

{{ count + 1 }}

無法解包

{{ count + 1 }}

為了解決這個問題,我們可以將 id 解構為一個頂級屬性:

const { id } = object
{{ id + 1 }}

現在渲染的結果將是 2。

另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }} 標籤),那麼它將被解包,因此以下內容將渲染為 1:

{{ object.id }}

上一篇
vue3鍊成術第十五天-Tamplate語法(2)
下一篇
vue3鍊成術第十七天-計算屬性(補充)
系列文
自學vue~點亮Roadmap過程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言